Completed
Push — 16.1 ( a69d3d...f10c6f )
by Nathan
27:43 queued 13:57
created

et2_widget_planner.js ➔ ???   B

Complexity

Conditions 1
Paths 0

Size

Total Lines 2413

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 1
c 4
b 1
f 0
nc 0
nop 0
dl 0
loc 2413
rs 8.2857

56 Functions

Rating   Name   Duplication   Size   Complexity  
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.user.headers 0 29 5
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.doLoadingFinished 0 206 2
C et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.user.group 0 58 10
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.init 0 41 1
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.user.title 0 1 1
C et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.user.row_labels 0 74 15
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.destroy 0 9 2
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.event_change 0 27 2
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.getDetachedAttributes 0 3 1
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.user.draw_row 0 24 5
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.getDOMNode 0 11 4
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._drawRow 0 19 2
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.month.row_labels 0 12 2
B et2_widget_planner.js ➔ ... ➔ set_group_by 0 16 6
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._drag_helper 0 19 2
C et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._header_hours 0 37 8
D et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._mouse_down 0 37 9
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._get_action_links 0 23 5
D et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.click 0 81 22
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._header_weeks 0 56 9
D et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._get_time_from_position 0 103 20
A et2_widget_planner.js ➔ ... ➔ set_end_date 0 5 1
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.month.draw_row 0 14 1
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.getDetachedNodes 0 3 1
F et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._init_links_dnd 0 180 15
C et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.day_class_holiday 0 46 9
A et2_widget_planner.js ➔ ... ➔ set_value 0 14 2
C et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.category.row_labels 0 62 11
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.category.headers 0 30 5
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.category.draw_row 0 8 3
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.invalidate 0 36 5
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.setDetachedAttributes 0 12 3
C et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._header_days 0 39 8
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._event_drop 0 46 3
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._link_actions 0 160 6
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._header_day_of_month 0 30 2
A et2_widget_planner.js ➔ ... ➔ set_show_weekend 0 12 4
A et2_widget_planner.js ➔ ... ➔ set_start_date 0 5 1
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._header_months 0 38 6
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.change 0 15 4
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._fetch_data 0 28 3
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.detachFromDOM 0 6 1
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.month.headers 0 3 1
C et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.month.group 0 51 12
A et2_widget_planner.js ➔ ... ➔ set_hide_empty 0 4 1
C et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.category.group 0 42 13
D et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._drawGrid 0 82 15
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.month.title 0 1 1
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._mouse_up 0 14 3
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.groupers.category.title 0 1 1
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.attachToDOM 0 18 1
C et2_widget_planner.js ➔ ... ➔ _cache_register 0 110 7
B et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend._deferred_row_update 0 39 6
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.resize 0 15 1
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.beforePrint 0 17 3
A et2_widget_planner.js ➔ ... ➔ et2_calendar_view.extend.afterPrint 0 3 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
/*
2
 * Egroupware Calendar timegrid
3
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
4
 * @package etemplate
5
 * @subpackage api
6
 * @link http://www.egroupware.org
7
 * @author Nathan Gray
8
 * @version $Id$
9
 */
10
11
12
/*egw:uses
13
	/calendar/js/et2_widget_view.js;
14
	/calendar/js/et2_widget_planner_row.js;
15
	/calendar/js/et2_widget_event.js;
16
*/
17
18
/**
19
 * Class which implements the "calendar-planner" XET-Tag for displaying a longer
20
 * ( > 10 days) span of time.  Events can be grouped into rows by either user,
21
 * category, or month.  Their horizontal position and size in the row is determined
22
 * by their start date and duration relative to the displayed date range.
23
 *
24
 * @augments et2_calendar_view
25
 */
26
var et2_calendar_planner = (function(){ "use strict"; return et2_calendar_view.extend([et2_IDetachedDOM, et2_IResizeable, et2_IPrint],
27
{
28
	createNamespace: true,
29
30
	attributes: {
31
		group_by: {
32
			name: "Group by",
33
			type: "string", // or category ID
34
			default: "0",
35
			description: "Display planner by 'user', 'month', or the given category"
36
		},
37
		filter: {
38
			name: "Filter",
39
			type: "string",
40
			default: '',
41
			description: 'A filter that is used to select events.  It is passed along when events are queried.'
42
		},
43
		show_weekend: {
44
			name: "Weekends",
45
			type: "boolean",
46
			default: egw.preference('days_in_weekview','calendar') != 5,
47
			description: "Display weekends.  The date range should still include them for proper scrolling, but they just won't be shown."
48
		},
49
		hide_empty: {
50
			name: "Hide empty rows",
51
			type: "boolean",
52
			default: false,
53
			description: "Hide rows with no events."
54
		},
55
		value: {
56
			type: "any",
57
			description: "A list of events, optionally you can set start_date, end_date and group_by as keys and events will be fetched"
58
		},
59
		"onchange": {
60
			"name": "onchange",
61
			"type": "js",
62
			"default": et2_no_init,
63
			"description": "JS code which is executed when the date range changes."
64
		},
65
		"onevent_change": {
66
			"name": "onevent_change",
67
			"type": "js",
68
			"default": et2_no_init,
69
			"description": "JS code which is executed when an event changes."
70
		}
71
	},
72
73
	DEFERRED_ROW_TIME: 100,
74
75
	/**
76
	 * Constructor
77
	 *
78
	 * @memberOf et2_calendar_planner
79
	 * @constructor
80
	 */
81
	init: function() {
82
		this._super.apply(this, arguments);
83
84
		// Main container
85
		this.div = jQuery(document.createElement("div"))
86
			.addClass("calendar_plannerWidget");
87
88
		// Header
89
		this.gridHeader = jQuery(document.createElement("div"))
90
			.addClass("calendar_plannerHeader")
91
			.appendTo(this.div);
92
		this.headerTitle = jQuery(document.createElement("div"))
93
			.addClass("calendar_plannerHeaderTitle")
94
			.appendTo(this.gridHeader);
95
		this.headers = jQuery(document.createElement("div"))
96
			.addClass("calendar_plannerHeaderRows")
97
			.appendTo(this.gridHeader);
98
99
		this.rows = jQuery(document.createElement("div"))
100
			.addClass("calendar_plannerRows")
101
			.appendTo(this.div);
102
		this.grid = jQuery(document.createElement("div"))
103
			.addClass("calendar_plannerGrid")
104
			.appendTo(this.div);
105
106
		this.vertical_bar = jQuery(document.createElement("div"))
107
			.addClass('verticalBar')
108
			.appendTo(this.div);
109
110
		this.value = [];
111
112
		// Update timer, to avoid redrawing twice when changing start & end date
113
		this.update_timer = null;
114
		this.doInvalidate = true;
115
116
		this.setDOMNode(this.div[0]);
117
118
		this.registeredCallbacks = [];
119
		this.cache = {};
120
		this._deferred_row_updates = {};
121
	},
122
123
	destroy: function() {
124
		this._super.apply(this, arguments);
125
		this.div.off();
126
127
		for(var i = 0; i < this.registeredCallbacks.length; i++)
128
		{
129
			egw.dataUnregisterUID(this.registeredCallbacks[i],false,this);
130
		}
131
	},
132
133
	doLoadingFinished: function() {
134
		this._super.apply(this, arguments);
135
136
		// Don't bother to draw anything if there's no date yet
137
		if(this.options.start_date)
138
		{
139
			this._drawGrid();
140
		}
141
142
		// Automatically bind drag and resize for every event using jQuery directly
143
		// - no action system -
144
		var planner = this;
145
146
		this.cache = {};
147
		this._deferred_row_updates = {};
148
		
149
		/**
150
		 * If user puts the mouse over an event, then we'll set up resizing so
151
		 * they can adjust the length.  Should be a little better on resources
152
		 * than binding it for every calendar event.
153
		 */
154
		this.div.on('mouseover', '.calendar_calEvent:not(.ui-resizable):not(.rowNoEdit)', function() {
155
				// Load the event
156
				planner._get_event_info(this);
157
				var that = this;
158
159
				//Resizable event handler
160
				jQuery(this).resizable
161
				({
162
					distance: 10,
163
					grid: [5, 10000],
164
					autoHide: false,
165
					handles: 'e',
166
					containment:'parent',
167
168
					/**
169
					 *  Triggered when the resizable is created.
170
					 *
171
					 * @param {event} event
172
					 * @param {Object} ui
173
					 */
174
					create:function(event, ui)
175
					{
176
						var resizeHelper = event.target.getAttribute('data-resize');
177
						if (resizeHelper == 'WD' || resizeHelper == 'WDS')
178
						{
179
							jQuery(this).resizable('destroy');
180
						}
181
					},
182
183
					/**
184
					 * If dragging to resize an event, abort drag to create
185
					 *
186
					 * @param {jQuery.Event} event
187
					 * @param {Object} ui
188
					 */
189
					start: function(event, ui)
190
					{
191
						if(planner.drag_create.start)
192
						{
193
							// Abort drag to create, we're dragging to resize
194
							planner._drag_create_end({});
195
						}
196
					},
197
198
					/**
199
					 * Triggered at the end of resizing the calEvent.
200
					 *
201
					 * @param {event} event
202
					 * @param {Object} ui
203
					 */
204
					stop:function(event, ui)
205
					{
206
						var e = new jQuery.Event('change');
207
						e.originalEvent = event;
208
						e.data = {duration: 0};
209
						var event_data = planner._get_event_info(this);
210
						var event_widget = planner.getWidgetById(event_data.widget_id);
211
						var sT = event_widget.options.value.start_m;
212
						if (typeof this.dropEnd != 'undefined')
213
						{
214
							var eT = parseInt(this.dropEnd.getUTCHours() * 60) + parseInt(this.dropEnd.getUTCMinutes());
215
							e.data.duration = ((eT - sT)/60) * 3600;
216
217
							if(event_widget)
218
							{
219
								event_widget.options.value.end_m = eT;
220
								event_widget.options.value.duration = e.data.duration;
221
							}
222
223
							// Leave the helper there until the update is done
224
							var loading = ui.helper.clone().appendTo(ui.helper.parent());
225
226
							// and add a loading icon so user knows something is happening
227
							jQuery('.calendar_timeDemo',loading).after('<div class="loading"></div>');
228
229
							jQuery(this).trigger(e);
230
231
							// That cleared the resize handles, so remove for re-creation...
232
							jQuery(this).resizable('destroy');
233
234
							// Remove loading, done or not
235
							loading.remove();
236
						}
237
						// Clear the helper, re-draw
238
						if(event_widget)
239
						{
240
							event_widget._parent.position_event(event_widget);
241
						}
242
					},
243
244
					/**
245
					 * Triggered during the resize, on the drag of the resize handler
246
					 *
247
					 * @param {event} event
248
					 * @param {Object} ui
249
					 */
250
					resize:function(event, ui)
251
					{
252
						if(planner.options.group_by == 'month')
253
						{
254
							var position = {left: event.clientX, top: event.clientY};
255
						}
256
						else
257
						{
258
							var position = {top:ui.position.top, left: ui.position.left + ui.helper.width()};
259
						}
260
						planner._drag_helper(this,position,ui.helper.outerHeight());
261
					}
262
				});
263
			})
264
			.on('mousemove', function(event) {
265
				// Ignore headers
266
				if(planner.headers.has(event.target).length !== 0)
267
				{
268
					planner.vertical_bar.hide();
269
					return;
270
				}
271
				// Position bar by mouse
272
				planner.vertical_bar.position({
273
					my: 'right-1',
274
					of: event,
275
					collision: 'fit'
276
				});
277
				planner.vertical_bar.css('top','0px');
278
279
				// Get time at mouse
280
				if(jQuery(event.target).closest('.calendar_eventRows').length == 0)
281
				{
282
					// "Invalid" times, from space after the last planner row, or header
283
					var time = planner._get_time_from_position(event.pageX - planner.grid.offset().left, 10);
284
				}
285
				else if(planner.options.group_by == 'month')
286
				{
287
					var time = planner._get_time_from_position(event.clientX, event.clientY);
288
				}
289
				else
290
				{
291
					var time = planner._get_time_from_position(event.offsetX, event.offsetY);
292
				}
293
				// Passing to formatter, cancel out timezone
294
				if(time)
295
				{
296
					var formatDate = new Date(time.valueOf() + time.getTimezoneOffset() * 60 * 1000);
297
					planner.vertical_bar
298
						.html('<span>'+date(egw.preference('timeformat','calendar') == 12 ? 'h:ia' : 'H:i',formatDate)+'</span>')
299
						.show();
300
301
					if(planner.drag_create.event && planner.drag_create.parent && planner.drag_create.end)
302
					{
303
304
						planner.drag_create.end.date = time.toJSON()
305
						planner._drag_update_event();
306
					}
307
				}
308
				else
309
				{
310
					// No (valid) time, just hide
311
					planner.vertical_bar.hide();
312
				}
313
			})
314
			.on('mousedown', jQuery.proxy(this._mouse_down, this))
315
			.on('mouseup', jQuery.proxy(this._mouse_up, this));
316
317
		// Actions may be set on a parent, so we need to explicitly get in here
318
		// and get ours
319
		this._link_actions(this.options.actions || this._parent.options.actions || []);
320
321
		// Customize and override some draggable settings
322
		this.div.on('dragcreate','.calendar_calEvent', function(event, ui) {
323
				jQuery(this).draggable('option','cancel','.rowNoEdit');
324
				// Act like you clicked the header, makes it easier to position
325
				jQuery(this).draggable('option','cursorAt', {top: 5, left: 5});
326
			})
327
			.on('dragstart', '.calendar_calEvent', function(event,ui) {
328
				jQuery('.calendar_calEvent',ui.helper).width(jQuery(this).width())
329
					.height(jQuery(this).outerHeight())
330
					.css('top', '').css('left','')
331
					.appendTo(ui.helper);
332
				ui.helper.width(jQuery(this).width());
333
334
				// Cancel drag to create, we're dragging an existing event
335
				planner._drag_create_end();
336
			});
337
		return true;
338
	},
339
340
	/**
341
	 * These handle the differences between the different group types.
342
	 * They provide the different titles, labels and grouping
343
	 */
344
	groupers: {
345
		// Group by user has one row for each user
346
		user:
347
		{
348
			// Title in top left corner
349
			title: function() { return this.egw().lang('User');},
350
			// Column headers
351
			headers: function() {
352
				var start = new Date(this.options.start_date);
353
				var end = new Date(this.options.end_date);
354
				var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(),start.getUTCDate());
355
				var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(),end.getUTCDate());
356
				var day_count = Math.round((end_date - start_date) /(1000*3600*24))+1;
357
				if(day_count >= 6)
358
				{
359
					this.headers.append(this._header_months(start, day_count));
360
				}
361
				if(day_count < 120)
362
				{
363
					var weeks = this._header_weeks(start, day_count);
364
					this.headers.append(weeks);
365
					this.grid.append(weeks);
366
				}
367
				if(day_count < 60)
368
				{
369
					var days = this._header_days(start, day_count);
370
					this.headers.append(days);
371
					this.grid.append(days);
372
				}
373
				if(day_count <= 7)
374
				{
375
					var hours = this._header_hours(start, day_count);
376
					this.headers.append(hours);
377
					this.grid.append(hours);
378
				}
379
			},
380
			// Labels for the rows
381
			row_labels: function() {
382
				var labels = [];
383
				var already_added = [];
384
				var options = false;
385
				if(app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner'))
386
				{
387
					options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection();
388
				}
389
				else
390
				{
391
					options = this.getArrayMgr("sel_options").getRoot().getEntry('owner');
392
				}
393
				for(var i = 0; i < this.options.owner.length; i++)
394
				{
395
					var user = this.options.owner[i];
396
					// Handle grouped resources like mailing lists - pull it from sidebox owner
397
					// and expand to their contents
398
					if(isNaN(user) && options && options.find)
399
					{
400
						var resource = options.find(function(element) {return element.id == user;}) || {};
401
						if(resource && resource.resources)
402
						{
403
							for(var j = 0; j < resource.resources.length; j++)
404
							{
405
								var id = resource.resources[j];
406
								if(already_added.indexOf(''+id) < 0)
407
								{
408
									labels.push({
409
										id: id,
410
										label: this._get_owner_name(id)||'',
411
										data: {participants:id,owner:id}
412
									});
413
									already_added.push(''+id);
414
								}
415
							}
416
						}
417
						else if(already_added.indexOf(''+user) < 0)
418
						{
419
							labels.push({
420
								id: user,
421
								label: this._get_owner_name(user),
422
								data: {participants:user,owner:user}
423
							});
424
							already_added.push(''+user);
425
						}
426
					}
427
					else if (user < 0)	// groups
428
					{
429
						egw.accountData(user,'account_fullname',true,function(result) {
430
							for(var id in result)
431
							{
432
								if(already_added.indexOf(''+id) < 0)
433
								{
434
									this.push({id: id, label: result[id]||'', data: {participants:id,owner:id}});
435
									already_added.push(''+id);
436
								}
437
							}
438
						},labels);
439
					}
440
					else	// users
441
					{
442
						if(already_added.indexOf(user) < 0)
443
						{
444
							var label = this._get_owner_name(user)||'';
445
							labels.push({id: user, label: label, data: {participants:user,owner:''}});
446
							already_added.push(''+user);
447
						}
448
					}
449
				}
450
451
				return labels.sort(function(a,b) {
452
					return a.label.localeCompare(b.label);
453
				});
454
			},
455
			// Group the events into the rows
456
			group: function(labels, rows, event) {
457
				// convert filter to allowed status
458
				var status_to_show = ['U','A','T','D','G'];
459
				switch(this.options.filter)
460
				{
461
					case 'unknown':
462
						status_to_show = ['U','G']; break;
463
					case 'accepted':
464
						status_to_show = ['A']; break;
465
					case 'tentative':
466
						status_to_show = ['T']; break;
467
					case 'rejected':
468
						status_to_show = ['R']; break;
469
					case 'delegated':
470
						status_to_show = ['D']; break;
471
					case 'all':
472
						status_to_show = ['U','A','T','D','G','R']; break;
473
					default:
474
						status_to_show = ['U','A','T','D','G']; break;
475
				}
476
				var participants = event.participants;
477
				var add_row = function(user, participant) {
478
					var label_index = false;
479
					for(var i = 0; i < labels.length; i++)
480
					{
481
						if(labels[i].id == user)
482
						{
483
							label_index = i;
484
							break;
485
						}
486
					}
487
					if(participant && label_index !== false && status_to_show.indexOf(participant.substr(0,1)) >= 0 ||
488
						this.options.filter === 'owner' && event.owner === user)
489
					{
490
						if(typeof rows[label_index] === 'undefined')
491
						{
492
							rows[label_index] = [];
493
						}
494
						rows[label_index].push(event);
495
					}
496
				};
497
				for(var user in participants)
498
				{
499
					var participant = participants[user];
500
					if (parseInt(user) < 0)	// groups
501
					{
502
						var planner = this;
503
						egw.accountData(user,'account_fullname',true,function(result) {
504
							for(var id in result)
505
							{
506
								if(!participants[id]) add_row.call(planner,id,participant);
507
							}
508
						},labels);
509
						continue;
510
					}
511
					add_row.call(this, user, participant);
512
				}
513
			},
514
			// Draw a single row
515
			draw_row: function(sort_key, label, events) {
516
				var row = this._drawRow(sort_key, label,events,this.options.start_date, this.options.end_date);
517
				if(this.options.hide_empty && !events.length)
518
				{
519
					row.set_disabled(true);
520
				}
521
				// Highlight current user, sort_key is account_id
522
				if(sort_key === egw.user('account_id'))
523
				{
524
					row.set_class('current_user')
525
				}
526
				// Since the daywise cache is by user, we can tap in here
527
				var t = new Date(this.options.start_date);
528
				var end = new Date(this.options.end_date);
529
				do
530
				{
531
					var cache_id = app.classes.calendar._daywise_cache_id(t, sort_key);
532
					egw.dataRegisterUID(cache_id, row._data_callback, row);
533
534
					t.setUTCDate(t.getUTCDate() + 1);
535
				}
536
				while(t < end);
537
				return row;
538
			}
539
		},
540
541
		// Group by month has one row for each month
542
		month:
543
		{
544
			title: function() { return this.egw().lang('Month');},
545
			headers: function() {
546
				this.headers.append(this._header_day_of_month());
547
			},
548
			row_labels: function() {
549
				var labels = [];
550
				var d = new Date(this.options.start_date);
551
				d = new Date(d.valueOf() + d.getTimezoneOffset() * 60 * 1000);
552
				for(var i = 0; i < 12; i++)
553
				{
554
					// Not using UTC because we corrected for timezone offset
555
					labels.push({id: d.getFullYear() +'-'+d.getMonth(), label:this.egw().lang(date('F',d))+' '+d.getFullYear()});
556
					d.setMonth(d.getMonth()+1);
557
				}
558
				return labels;
559
			},
560
			group: function(labels, rows,event) {
561
				// Yearly planner does not show infologs
562
				if(event && event.app && event.app == 'infolog') return;
563
564
				var start = new Date(event.start);
565
				start = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000);
566
				var key = start.getFullYear() +'-'+start.getMonth();
567
				var label_index = false;
568
				for(var i = 0; i < labels.length; i++)
569
				{
570
					if(labels[i].id == key)
571
					{
572
						label_index = i;
573
						break;
574
					}
575
				}
576
				if(typeof rows[label_index] === 'undefined')
577
				{
578
					rows[label_index] = [];
579
				}
580
				rows[label_index].push(event);
581
582
				// end in a different month?
583
				var end = new Date(event.end);
584
				end = new Date(end.valueOf() + end.getTimezoneOffset() * 60 * 1000);
585
				var end_key = end.getFullYear() +'-'+end.getMonth();
586
				var year = start.getFullYear();
587
				var month = start.getMonth();
588
				while(key !== end_key)
589
				{
590
					if (++month > 11)
591
					{
592
						++year;
593
						month = 0;
594
					}
595
					key = sprintf('%04d-%d',year,month);
596
					for(var i = 0; i < labels.length; i++)
597
					{
598
						if(labels[i].id == key)
599
						{
600
							label_index = i;
601
							if(typeof rows[label_index] === 'undefined')
602
							{
603
								rows[label_index] = [];
604
							}
605
							break;
606
						}
607
					}
608
					rows[label_index].push(event);
609
				}
610
			},
611
			// Draw a single row, but split up the dates
612
			draw_row: function(sort_key, label, events)
613
			{
614
				var key = sort_key.split('-');
615
				var start = new Date(key[0]+"-"+sprintf("%02d",parseInt(key[1])+1)+"-01T00:00:00Z");
616
				// Use some care to avoid issues with timezones and daylight savings
617
				var end = new Date(start);
618
				end.setUTCMonth(start.getUTCMonth() + 1);
619
				end.setUTCDate(1);
620
				end.setUTCHours(0);
621
				end.setUTCMinutes(0);
622
				end = new Date(end.valueOf() - 1000);
623
				end.setUTCMonth(start.getUTCMonth())
624
				this._drawRow(sort_key, label, events, start, end);
625
			}
626
		},
627
		// Group by category has one row for each [sub]category
628
		category:
629
		{
630
			title: function() { return this.egw().lang('Category');},
631
			headers: function() {
632
				var start = new Date(this.options.start_date);
633
				var end = new Date(this.options.end_date);
634
				var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(),start.getUTCDate());
635
				var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(),end.getUTCDate());
636
				var day_count = Math.round((end_date - start_date) /(1000*3600*24))+1;
637
638
				if(day_count >= 6)
639
				{
640
					this.headers.append(this._header_months(start, day_count));
641
				}
642
				if(day_count < 120)
643
				{
644
					var weeks = this._header_weeks(start, day_count);
645
					this.headers.append(weeks);
646
					this.grid.append(weeks);
647
				}
648
				if(day_count < 60)
649
				{
650
					var days = this._header_days(start, day_count);
651
					this.headers.append(days);
652
					this.grid.append(days);
653
				}
654
				if(day_count <= 7)
655
				{
656
					var hours = this._header_hours(start, day_count);
657
					this.headers.append(hours);
658
					this.grid.append(hours);
659
				}
660
			},
661
			row_labels: function() {
662
				var im = this.getInstanceManager();
663
				var categories = et2_selectbox.cat_options({
664
						_type:'select-cat',
665
						getInstanceManager: function() {return im;}
666
					},{application: 'calendar'});
667
668
				var labels = [];
669
				if(!app.calendar.state.cat_id ||
670
					app.calendar.state.cat_id.toString() === '' ||
671
					app.calendar.state.cat_id.toString() == '0'
672
				)
673
				{
674
					app.calendar.state.cat_id = '';
675
					labels.push({id:'',value:'',label: egw.lang('none'), main: '', data: {}});
676
					labels = labels.concat(categories);
677
				}
678
				else
679
				{
680
					var cat_id = app.calendar.state.cat_id;
681
					if(typeof cat_id == 'string')
682
					{
683
						cat_id = cat_id.split(',');
684
					}
685
					for(var i = 0; i < cat_id.length; i++)
686
					{
687
						// Find label for that category
688
						for(var j = 0; j < categories.length; j++)
689
						{
690
							if(categories[j].value == cat_id[i])
691
							{
692
								categories[j].id = categories[j].value;
693
								labels.push(categories[j]);
694
								break;
695
							}
696
						}
697
698
						// Get its children immediately
699
						egw.json(
700
							'EGroupware\\Api\\Etemplate\\Widget\\Select::ajax_get_options',
701
							['select-cat',',,,calendar,'+cat_id[i]],
702
							function(data) {
703
								labels = labels.concat(data);
704
							}
705
						).sendRequest(false);
706
					}
707
				}
708
709
				for(var i = labels.length -1; i >= 0; i--)
710
				{
711
					labels[i].id = labels[i].value;
712
					labels[i].data = {
713
						cat_id: labels[i].id,
714
						main: labels[i].value==labels[i].main
715
					};
716
					if(labels[i].children && labels[i].children.length)
717
					{
718
						labels[i].data.has_children = true;
719
					}
720
				}
721
				return labels;
722
			},
723
			group: function(labels, rows, event) {
724
				var cats = event.category;
725
				if(typeof event.category === 'string')
726
				{
727
					cats = cats.split(',');
728
				}
729
				for(var cat = 0; cat < cats.length; cat++)
730
				{
731
					var label_index = false;
732
					var category = cats[cat] ? parseInt(cats[cat],10) : false;
733
					if(category == 0 || !category) category = '';
734
					for(var i = 0; i < labels.length; i++)
735
					{
736
						if(labels[i].id == category)
737
						{
738
							// If there's no cat filter, only show the top level
739
							if(!app.calendar.state.cat_id)
740
							{
741
								for(var j = 0; j < labels.length; j++)
742
								{
743
									if(labels[j].id == labels[i].main)
744
									{
745
										label_index = j;
746
										break;
747
									}
748
								}
749
								break;
750
							}
751
							label_index = i;
752
							break;
753
						}
754
					}
755
					if(typeof rows[label_index] === 'undefined')
756
					{
757
						rows[label_index] = [];
758
					}
759
					if(rows[label_index].indexOf(event) === -1)
760
					{
761
						rows[label_index].push(event);
762
					}
763
				}
764
			},
765
			draw_row: function(sort_key, label, events) {
766
				var row = this._drawRow(sort_key, label,events,this.options.start_date, this.options.end_date);
767
				if(this.options.hide_empty && !events.length)
768
				{
769
					row.set_disabled(true);
770
				}
771
				return row;
772
			}
773
		}
774
	},
775
776
	/**
777
	 * Something changed, and the planner needs to be re-drawn.  We wait a bit to
778
	 * avoid re-drawing twice if start and end date both changed, then recreate.
779
	 *
780
	 * @param {boolean} trigger =false Trigger an event once things are done.
781
	 *	Waiting until invalidate completes prevents 2 updates when changing the date range.
782
	 * @returns {undefined}
783
	 */
784
	invalidate: function(trigger) {
785
786
		// Busy
787
		if(!this.doInvalidate) return;
788
789
		// Not yet ready
790
		if(!this.options.start_date || !this.options.end_date) return;
791
792
		// Wait a bit to see if anything else changes, then re-draw the days
793
		if(this.update_timer !== null)
794
		{
795
			window.clearTimeout(this.update_timer);
796
		}
797
		this.update_timer = window.setTimeout(jQuery.proxy(function() {
798
			this.widget.doInvalidate = false;
799
800
			// Show AJAX loader
801
			this.widget.loader.show();
802
803
			this.widget.cache = {};
804
			this._deferred_row_updates = {};
805
806
			this.widget._fetch_data();
807
808
			this.widget._drawGrid();
809
810
			if(this.trigger)
811
			{
812
				this.widget.change();
813
			}
814
			this.widget.update_timer = null;
815
			this.widget.doInvalidate = true;
816
817
			window.setTimeout(jQuery.proxy(function() {if(this.loader) this.loader.hide();},this.widget),500);
818
		},{widget:this,"trigger":trigger}),ET2_GRID_INVALIDATE_TIMEOUT);
819
	},
820
821
	detachFromDOM: function() {
822
		// Remove the binding to the change handler
823
		jQuery(this.div).off("change.et2_calendar_timegrid");
824
825
		this._super.apply(this, arguments);
826
	},
827
828
	attachToDOM: function() {
829
		this._super.apply(this, arguments);
830
831
		// Add the binding for the event change handler
832
		jQuery(this.div).on("change.et2_calendar_timegrid", '.calendar_calEvent', this, function(e) {
833
			// Make sure function gets a reference to the widget
834
			var args = Array.prototype.slice.call(arguments);
835
			if(args.indexOf(this) == -1) args.push(this);
836
837
			return e.data.event_change.apply(e.data, args);
838
		});
839
840
		// Add the binding for the change handler
841
		jQuery(this.div).on("change.et2_calendar_timegrid", '*:not(.calendar_calEvent)', this, function(e) {
842
				return e.data.change.call(e.data, e, this);
843
			});
844
845
	},
846
847
	getDOMNode: function(_sender)
848
	{
849
		if(_sender === this || !_sender)
850
		{
851
			return this.div[0];
852
		}
853
		if(_sender._parent === this)
854
		{
855
			return this.rows[0];
856
		}
857
	},
858
859
	/**
860
	 * Creates all the DOM nodes for the planner grid
861
	 *
862
	 * Any existing nodes (& children) are removed, the headers & labels are
863
	 * determined according to the current group_by value, and then the rows
864
	 * are created.
865
	 *
866
	 * @method
867
	 * @private
868
	 *
869
	 */
870
	_drawGrid: function()
871
	{
872
873
		this.div.css('height', this.options.height);
874
875
		// Clear old events
876
		var delete_index = this._children.length - 1;
877
		while(this._children.length > 0 && delete_index >= 0)
878
		{
879
			this._children[delete_index].free();
880
			this.removeChild(this._children[delete_index--]);
881
		}
882
883
		// Clear old rows
884
		this.rows.empty()
885
			.append(this.grid);
886
		this.grid.empty();
887
888
		var grouper = this.grouper;
889
		if(!grouper) return;
890
891
		// Headers
892
		this.headers.empty();
893
		this.headerTitle.text(grouper.title.apply(this));
894
		grouper.headers.apply(this);
895
		this.grid.find('*').contents().filter(function(){
896
			return this.nodeType === 3;
897
		}).remove();
898
899
		// Get the rows / labels
900
		var labels = grouper.row_labels.call(this);
901
902
		// Group the events
903
		var events = {};
904
		for(var i = 0; i < this.value.length; i++)
905
		{
906
			grouper.group.call(this, labels, events, this.value[i]);
907
		}
908
909
		// Set height for rows
910
		this.rows.height(this.div.height() - this.headers.outerHeight());
911
912
		// Draw the rows
913
		for(var key in labels)
914
		{
915
			if (!labels.hasOwnProperty(key)) continue;
916
917
			// Skip sub-categories (events are merged into top level)
918
			if(this.options.group_by == 'category' &&
919
				(!app.calendar.state.cat_id || app.calendar.state.cat_id == '') &&
920
				labels[key].id != labels[key].main
921
			)
922
			{
923
				continue;
924
			}
925
			var row = grouper.draw_row.call(this,labels[key].id, labels[key].label, events[key] || []);
926
927
			// Add extra data for clicking on row
928
			if(row)
929
			{
930
				for(var extra in labels[key].data)
931
				{
932
					row.getDOMNode().dataset[extra] = labels[key].data[extra];
933
				}
934
			}
935
		}
936
937
		// Adjust header if there's a scrollbar
938
		if(this.rows.children().last().length)
939
		{
940
			this.gridHeader.css('margin-right', (this.rows.width() - this.rows.children().last().width()) + 'px');
941
		}
942
		// Add actual events
943
		for(var key in this._deferred_row_updates)
944
		{
945
			window.clearTimeout(key);
946
		}
947
		window.setTimeout(jQuery.proxy(function() {
948
			this._deferred_row_update();
949
		}, this ),this.DEFERRED_ROW_TIME)
950
		this.value = [];
951
	},
952
953
	/**
954
	 * Draw a single row of the planner
955
	 *
956
	 * @param {string} key Index into the grouped labels & events
957
	 * @param {string} label
958
	 * @param {Array} events
959
	 * @param {Date} start
960
	 * @param {Date} end
961
	 */
962
	_drawRow: function(key, label, events, start, end)
963
	{
964
		var row = et2_createWidget('calendar-planner_row',{
965
				id: 'planner_row_'+key,
966
				label: label,
967
				start_date: start,
968
				end_date: end,
969
				value: events,
970
				readonly: this.options.readonly
971
			},this);
972
973
974
		if(this.isInTree())
975
		{
976
			row.doLoadingFinished();
977
		}
978
979
		return row;
980
	},
981
982
983
	_header_day_of_month: function()
984
	{
985
		var day_width = 3.23; // 100.0 / 31;
986
987
		// month scale with navigation
988
		var content = '<div class="calendar_plannerScale">';
989
		var start = new Date(this.options.start_date);
990
		start = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000);
991
		var end = new Date(this.options.end_date);
992
		end = new Date(end.valueOf() + end.getTimezoneOffset() * 60 * 1000);
993
994
		var title = this.egw().lang(date('F',start))+' '+date('Y',start)+' - '+
995
			this.egw().lang(date('F',end))+' '+date('Y',end);
996
997
		content += '<div class="calendar_plannerMonthScale th et2_link" style="left: 0; width: 100%;">'+
998
				title+"</div>";
999
		content += "</div>";		// end of plannerScale
1000
1001
		// day of month scale
1002
		content +='<div class="calendar_plannerScale">';
1003
1004
		for(var left = 0, i = 0; i < 31; left += day_width,++i)
1005
		{
1006
			content += '<div class="calendar_plannerDayOfMonthScale " style="left: '+left+'%; width: '+day_width+'%;">'+
1007
				(1+i)+"</div>\n";
1008
		}
1009
		content += "</div>\n";
1010
1011
		return content;
1012
	},
1013
1014
	/**
1015
	 * Make a header showing the months
1016
	 * @param {Date} start
1017
	 * @param {number} days
1018
	 * @returns {string} HTML snippet
1019
	 */
1020
	_header_months: function(start, days)
1021
	{
1022
		var content = '<div class="calendar_plannerScale">';
1023
		var days_in_month = 0;
1024
		var day_width = 100 / days;
1025
		var end = new Date(start);
1026
		end.setUTCDate(end.getUTCDate()+days);
1027
		var t = new Date(start.valueOf());
1028
		for(var left = 0,i = 0; i < days;t.setUTCDate(1),t.setUTCMonth(t.getUTCMonth()+1),left += days_in_month*day_width,i += days_in_month)
1029
		{
1030
			var u = new Date(t.getUTCFullYear(),t.getUTCMonth()+1,0,-t.getTimezoneOffset()/60);
1031
			days_in_month =  1+ ((u-t) / (24*3600*1000));
1032
1033
			var first = new Date(t.getUTCFullYear(),t.getUTCMonth(),1,-t.getTimezoneOffset()/60);
1034
			if(days_in_month <= 0) break;
1035
1036
			if (i + days_in_month > days)
1037
			{
1038
				days_in_month = days - i;
1039
			}
1040
			var title = this.egw().lang(date('F',new Date(t.valueOf() + t.getTimezoneOffset() * 60 * 1000)));
1041
			if (days_in_month > 10)
1042
			{
1043
				title += '</span> <span class="et2_clickable et2_link" data-sortby="month">'+t.getUTCFullYear();
1044
			}
1045
			else if (days_in_month < 5)
1046
			{
1047
				title = '&nbsp;';
1048
			}
1049
			content += '<div class="calendar_plannerMonthScale" data-date="'+first.toJSON()+
1050
				'" style="left: '+left+'%; width: '+(day_width*days_in_month)+'%;"><span'+
1051
				' data-planner_view="month" class="et2_clickable et2_link">'+
1052
				title+"</span></div>";
1053
		}
1054
		content += "</div>";		// end of plannerScale
1055
1056
		return content;
1057
	},
1058
1059
	/**
1060
	 * Make a header showing the week numbers
1061
	 *
1062
	 * @param {Date} start
1063
	 * @param {number} days
1064
	 * @returns {string} HTML snippet
1065
	 */
1066
	_header_weeks: function(start, days)
1067
	{
1068
1069
		var content = '<div class="calendar_plannerScale" data-planner_view="week">';
1070
		var state = '';
1071
1072
		// we're not using UTC so date() formatting function works
1073
		var t = new Date(start.valueOf());
1074
1075
		// Make sure we're lining up on the week
1076
		var week_end = app.calendar.date.end_of_week(start);
1077
		var days_in_week = Math.floor(((week_end-start ) / (24*3600*1000))+1);
1078
		var week_width = 100 / days * (days <= 7 ? days : days_in_week);
1079
		for(var left = 0,i = 0; i < days; t.setUTCDate(t.getUTCDate() + 7),left += week_width)
1080
		{
1081
			// Avoid overflow at the end
1082
			if(days - i < 7)
1083
			{
1084
				days_in_week = days-i;
1085
			}
1086
			var usertime = new Date(t.valueOf());
1087
			if(start.getTimezoneOffset() < 0)
1088
			{
1089
				// Gets the right week # east of GMT.  West does not need it(?)
1090
				usertime.setUTCMinutes(usertime.getUTCMinutes() - start.getTimezoneOffset());
1091
			}
1092
1093
			week_width = 100 / days * Math.min(days, days_in_week);
1094
1095
			var title = this.egw().lang('Week')+' '+app.calendar.date.week_number(usertime);
1096
1097
			if(start.getTimezoneOffset() > 0)
1098
			{
1099
				// Gets the right week start west of GMT
1100
				usertime.setUTCMinutes(usertime.getUTCMinutes() +start.getTimezoneOffset());
1101
			}
1102
			state = app.calendar.date.start_of_week(usertime);
1103
			state.setUTCHours(0);
1104
			state.setUTCMinutes(0);
1105
			state = state.toJSON();
1106
1107
			if(days_in_week > 1 || days == 1)
1108
			{
1109
				content += '<div class="calendar_plannerWeekScale et2_clickable et2_link" data-date=\'' + state + '\' style="left: '+left+'%; width: '+week_width+'%;">'+title+"</div>";
1110
			}
1111
			i+= days_in_week;
1112
			if(days_in_week != 7)
1113
			{
1114
				t.setUTCDate(t.getUTCDate() - (7 - days_in_week));
1115
				days_in_week = 7;
1116
			}
1117
		}
1118
		content += "</div>";		// end of plannerScale
1119
1120
		return content;
1121
	},
1122
1123
	/**
1124
	 * Make a header for some days
1125
	 *
1126
	 * @param {Date} start
1127
	 * @param {number} days
1128
	 * @returns {string} HTML snippet
1129
	 */
1130
	_header_days: function(start, days)
1131
	{
1132
		var day_width = 100 / days;
1133
		var content = '<div class="calendar_plannerScale'+(days > 3 ? 'Day' : '')+'" data-planner_view="day" >';
1134
1135
		// we're not using UTC so date() formatting function works
1136
		var t = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000);
1137
		for(var left = 0,i = 0; i < days; t.setDate(t.getDate()+1),left += day_width,++i)
1138
		{
1139
			if(!this.options.show_weekend && [0,6].indexOf(t.getDay()) !== -1 ) continue;
1140
			var holidays = [];
1141
			var tempDate = new Date(t);
1142
			tempDate.setMinutes(tempDate.getMinutes()-tempDate.getTimezoneOffset());
1143
			var title = '';
1144
			var state = '';
1145
1146
			if (days <= 3)
1147
			{
1148
				title = this.egw().lang(date('l',t))+', '+date('j',t)+'. '+this.egw().lang(date('F',t));
1149
			}
1150
			else if (days <= 7)
1151
			{
1152
				title = this.egw().lang(date('l',t))+' '+date('j',t);
1153
			}
1154
			else
1155
			{
1156
				title = this.egw().lang(date('D',t)).substr(0,2)+'<br />'+date('j',t);
1157
			}
1158
			state = new Date(t.valueOf() - t.getTimezoneOffset() * 60 * 1000);
1159
			var day_class = this.day_class_holiday(state,holidays);
1160
1161
			content += '<div class="calendar_plannerDayScale et2_clickable et2_link '+ day_class+
1162
				'" data-date=\'' + state.toJSON() +'\''+
1163
				(holidays ? ' title="'+holidays.join(',')+'"' : '')+'>'+title+"</div>\n";
1164
		}
1165
		content += "</div>";		// end of plannerScale
1166
1167
		return content;
1168
	},
1169
1170
	/**
1171
	 * Create a header with hours
1172
	 *
1173
	 * @param {Date} start
1174
	 * @param {number} days
1175
	 * @returns {string} HTML snippet for the header
1176
	 */
1177
	_header_hours: function(start,days)
1178
	{
1179
		var divisors = [1,2,3,4,6,8,12];
1180
		var decr = 1;
1181
		for(var i = 0; i < divisors.length; i++)	// numbers dividing 24 without rest
1182
		{
1183
			if (divisors[i] > days) break;
1184
			decr = divisors[i];
1185
		}
1186
		var hours = days * 24;
1187
		if (days === 1)			// for a single day we calculate the hours of a days, to take into account daylight saving changes (23 or 25 hours)
1188
		{
1189
			var t = new Date(start.getUTCFullYear(),start.getUTCMonth(),start.getUTCDate(),-start.getTimezoneOffset()/60);
1190
			var s = new Date(start);
1191
			s.setUTCHours(23);
1192
			s.setUTCMinutes(59);
1193
			s.setUTCSeconds(59);
1194
			hours = Math.ceil((s.getTime() - t.getTime()) / 3600000);
1195
		}
1196
		var cell_width = 100 / hours * decr;
1197
1198
		var content = '<div class="calendar_plannerScale" data-planner_view="day">';
1199
1200
		// we're not using UTC so date() formatting function works
1201
		var t = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000);
1202
		for(var left = 0,i = 0; i < hours; left += cell_width,i += decr)
1203
		{
1204
			if(!this.options.show_weekend && [0,6].indexOf(t.getDay()) !== -1 ) continue;
1205
			var title = date(egw.preference('timeformat','calendar') == 12 ? 'ha' : 'H',t);
1206
1207
			content += '<div class="calendar_plannerHourScale et2_link" data-date="' + t.toJSON() +'" style="left: '+left+'%; width: '+(cell_width)+'%;">'+title+"</div>";
1208
			t.setHours(t.getHours()+decr);
1209
		}
1210
		content += "</div>";		// end of plannerScale
1211
1212
		return content;
1213
	},
1214
1215
	/**
1216
	 * Applies class for today, and any holidays for current day
1217
	 *
1218
	 * @param {Date} date
1219
	 * @param {string[]} holiday_list Filled with a list of holidays for that day
1220
	 *
1221
	 * @return {string} CSS Classes for the day.  calendar_calBirthday, calendar_calHoliday, calendar_calToday and calendar_weekend as appropriate
1222
	 */
1223
	day_class_holiday: function(date,holiday_list) {
1224
1225
		if(!date) return '';
1226
1227
		var day_class = '';
1228
1229
		// Holidays and birthdays
1230
		var holidays = et2_calendar_view.get_holidays(this,date.getUTCFullYear());
1231
1232
		// Pass a string rather than the date object, to make sure it doesn't get changed
1233
		this.date_helper.set_value(date.toJSON());
1234
		var date_key = ''+this.date_helper.get_year() + sprintf('%02d',this.date_helper.get_month()) + sprintf('%02d',this.date_helper.get_date());
1235
		if(holidays && holidays[date_key])
1236
		{
1237
			holidays = holidays[date_key];
1238
			for(var i = 0; i < holidays.length; i++)
1239
			{
1240
				if (typeof holidays[i]['birthyear'] !== 'undefined')
1241
				{
1242
					day_class += ' calendar_calBirthday ';
1243
1244
					holiday_list.push(holidays[i]['name']);
1245
				}
1246
				else
1247
				{
1248
					day_class += 'calendar_calHoliday ';
1249
1250
					holiday_list.push(holidays[i]['name']);
1251
				}
1252
			}
1253
		}
1254
		holidays = holiday_list.join(',');
1255
		var today = new Date();
1256
		if(date_key === ''+today.getFullYear()+
1257
			sprintf("%02d",today.getMonth()+1)+
1258
			sprintf("%02d",today.getDate())
1259
		)
1260
		{
1261
			day_class += "calendar_calToday ";
1262
		}
1263
		if(date.getUTCDay() == 0 || date.getUTCDay() == 6)
1264
		{
1265
			day_class += "calendar_weekend ";
1266
		}
1267
		return day_class;
1268
	},
1269
1270
	/**
1271
	 * Link the actions to the DOM nodes / widget bits.
1272
	 *
1273
	 * @todo This currently does nothing
1274
	 * @param {object} actions {ID: {attributes..}+} map of egw action information
1275
	 */
1276
	_link_actions: function(actions)
1277
	{
1278
		if(!this._actionObject)
1279
		{
1280
			// Get the parent?  Might be a grid row, might not.  Either way, it is
1281
			// just a container with no valid actions
1282
			var objectManager = egw_getObjectManager(this.getInstanceManager().app,true,1);
1283
			objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId,2) || objectManager;
1284
			var parent = objectManager.getObjectById(this.id,3) || objectManager.getObjectById(this._parent.id,3) || objectManager;
1285
			if(!parent)
1286
			{
1287
				debugger;
0 ignored issues
show
Debugging Code introduced by
debugger looks like debug code. Are you sure you do not want to remove it?
Loading history...
1288
				egw.debug('error','No parent objectManager found');
1289
				return;
1290
			}
1291
1292
			for(var i = 0; i < parent.children.length; i++)
1293
			{
1294
				var parent_finder = jQuery('#'+this.div.id, parent.children[i].iface.doGetDOMNode());
1295
				if(parent_finder.length > 0)
1296
				{
1297
					parent = parent.children[i];
1298
					break;
1299
				}
1300
			}
1301
		}
1302
1303
		// This binds into the egw action system.  Most user interactions (drag to move, resize)
1304
		// are handled internally using jQuery directly.
1305
		var widget_object = this._actionObject || parent.getObjectById(this.id);
0 ignored issues
show
Bug introduced by
The variable parent does not seem to be initialized in case !this._actionObject on line 1278 is false. Are you sure this can never be the case?
Loading history...
1306
1307
		var aoi = new et2_action_object_impl(this,this.getDOMNode());
1308
1309
		/**
1310
		 * Determine if we allow a dropped event to use the invite/change actions,
1311
		 * and enable or disable them appropriately
1312
		 * 
1313
		 * @param {egwAction} action
1314
		 * @param {et2_calendar_event} event The event widget being dragged
1315
		 * @param {egwActionObject} target Planner action object
1316
		 */
1317
		var _invite_enabled = function(action, event, target)
1318
		{
1319
			var event = event.iface.getWidget();
1320
			var planner = target.iface.getWidget() || false;
1321
			//debugger;
1322
			if(event === planner || !event || !planner ||
1323
				!event.options || !event.options.value.participants || !planner.options.owner
1324
			)
1325
			{
1326
				return false;
1327
			}
1328
			var owner_match = false;
1329
			var own_row = false;
1330
1331
			for(var id in event.options.value.participants)
1332
			{
1333
				planner.iterateOver(function(row) {
1334
					// Check scroll section or header section
1335
					if(row.div.hasClass('drop-hover') || row.div.has(':hover'))
1336
					{
1337
						owner_match = owner_match || row.node.dataset[planner.options.group_by] === ''+id;
0 ignored issues
show
introduced by
The variable id is changed by the for-each loop on line 1331. Only the value of the last iteration will be visible in this function if it is called outside of the loop.
Loading history...
1338
						own_row = (row === event.getParent());
1339
					}
1340
				}, this, et2_calendar_planner_row);
1341
1342
			}
1343
			var enabled = !owner_match &&
1344
				// Not inside its own row
1345
				!own_row;
1346
1347
			widget_object.getActionLink('invite').enabled = enabled;
1348
			widget_object.getActionLink('change_participant').enabled = enabled;
1349
1350
			// If invite or change participant are enabled, drag is not
1351
			widget_object.getActionLink('egw_link_drop').enabled = !enabled;
1352
		};
1353
1354
		aoi.doTriggerEvent = function(_event, _data) {
1355
1356
			// Determine target node
1357
			var event = _data.event || false;
1358
			if(!event) return;
1359
			if(_data.ui.draggable.hasClass('rowNoEdit')) return;
1360
1361
			/*
1362
			We have to handle the drop in the normal event stream instead of waiting
1363
			for the egwAction system so we can get the helper, and destination
1364
			*/
1365
			if(event.type === 'drop')
1366
			{
1367
				this.getWidget()._event_drop.call(jQuery('.calendar_d-n-d_timeCounter',_data.ui.helper)[0],this.getWidget(),event, _data.ui);
1368
			}
1369
			var drag_listener = function(event, ui) {
1370
				aoi.getWidget()._drag_helper(jQuery('.calendar_d-n-d_timeCounter',ui.helper)[0],{
1371
						top:ui.position.top,
1372
						left: ui.position.left - jQuery(this).parent().offset().left
1373
					},0);
1374
			};
1375
			var time = jQuery('.calendar_d-n-d_timeCounter',_data.ui.helper);
1376
			switch(_event)
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
1377
			{
1378
				// Triggered once, when something is dragged into the timegrid's div
1379
				case EGW_AI_DRAG_OVER:
1380
					// Listen to the drag and update the helper with the time
1381
					// This part lets us drag between different timegrids
1382
					_data.ui.draggable.on('drag.et2_timegrid'+widget_object.id, drag_listener);
1383
					_data.ui.draggable.on('dragend.et2_timegrid'+widget_object.id, function() {
1384
						_data.ui.draggable.off('drag.et2_timegrid' + widget_object.id);
1385
					});
1386
					if(time.length)
1387
					{
1388
						// The out will trigger after the over, so we count
1389
						time.data('count',time.data('count')+1);
1390
					}
1391
					else
1392
					{
1393
						_data.ui.helper.prepend('<div class="calendar_d-n-d_timeCounter" data-count="1"><span></span></div>');
1394
					}
1395
1396
					break;
1397
1398
				// Triggered once, when something is dragged out of the timegrid
1399
				case EGW_AI_DRAG_OUT:
1400
					// Stop listening
1401
					_data.ui.draggable.off('drag.et2_timegrid'+widget_object.id);
1402
					// Remove any highlighted time squares
1403
					jQuery('[data-date]',this.doGetDOMNode()).removeClass("ui-state-active");
1404
1405
					// Out triggers after the over, count to not accidentally remove
1406
					time.data('count',time.data('count')-1);
1407
					if(time.length && time.data('count') <= 0)
1408
					{
1409
						time.remove();
1410
					}
1411
					break;
1412
			}
1413
		};
1414
1415
		if (widget_object == null) {
1416
			// Add a new container to the object manager which will hold the widget
1417
			// objects
1418
			widget_object = parent.insertObject(false, new egwActionObject(
0 ignored issues
show
Bug introduced by
The variable parent does not seem to be initialized in case !this._actionObject on line 1278 is false. Are you sure this can never be the case?
Loading history...
1419
				this.id, parent, aoi,
1420
				this._actionManager || parent.manager.getActionById(this.id) || parent.manager
1421
			),EGW_AO_FLAG_IS_CONTAINER);
1422
		}
1423
		else
1424
		{
1425
			widget_object.setAOI(aoi);
1426
		}
1427
		// Go over the widget & add links - this is where we decide which actions are
1428
		// 'allowed' for this widget at this time
1429
		var action_links = this._get_action_links(actions);
1430
1431
		this._init_links_dnd(widget_object.manager, action_links);
1432
1433
		widget_object.updateActionLinks(action_links);
1434
		this._actionObject = widget_object;
1435
	},
1436
1437
	/**
1438
	 * Automatically add dnd support for linking
1439
	 *
1440
	 * @param {type} mgr
1441
	 * @param {type} actionLinks
1442
	 */
1443
	_init_links_dnd: function(mgr,actionLinks) {
1444
1445
		if (this.options.readonly) return;
1446
		
1447
		var self = this;
1448
1449
		var drop_action = mgr.getActionById('egw_link_drop');
1450
		var drop_change_participant = mgr.getActionById('change_participant');
1451
		var drop_invite = mgr.getActionById('invite');
1452
		var drag_action = mgr.getActionById('egw_link_drag');
1453
		var paste_action = mgr.getActionById('egw_paste');
1454
1455
		// Disable paste action
1456
		if(paste_action == null)
1457
		{
1458
			paste_action = mgr.addAction('popup', 'egw_paste', egw.lang('Paste'), egw.image('editpaste'), function(){},true);
1459
		}
1460
		paste_action.set_enabled(false);
1461
1462
		// Check if this app supports linking
1463
		if(!egw.link_get_registry(this.dataStorePrefix || 'calendar', 'query') ||
1464
			egw.link_get_registry(this.dataStorePrefix || 'calendar', 'title'))
1465
		{
1466
			if(drop_action)
1467
			{
1468
				drop_action.remove();
1469
				if(actionLinks.indexOf(drop_action.id) >= 0)
1470
				{
1471
					actionLinks.splice(actionLinks.indexOf(drop_action.id),1);
1472
				}
1473
			}
1474
			if(drag_action)
1475
			{
1476
				drag_action.remove();
1477
				if(actionLinks.indexOf(drag_action.id) >= 0)
1478
				{
1479
					actionLinks.splice(actionLinks.indexOf(drag_action.id),1);
1480
				}
1481
			}
1482
			return;
1483
		}
1484
1485
		// Don't re-add
1486
		if(drop_action == null)
1487
		{
1488
			// Create the drop action that links entries
1489
			drop_action = mgr.addAction('drop', 'egw_link_drop', egw.lang('Create link'), egw.image('link'), function(action, source, dropped) {
1490
				// Extract link IDs
1491
				var links = [];
1492
				var id = '';
1493
				for(var i = 0; i < source.length; i++)
1494
				{
1495
					if(!source[i].id) continue;
1496
					id = source[i].id.split('::');
1497
					links.push({app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1]});
1498
				}
1499
				if(!links.length)
1500
				{
1501
					return;
1502
				}
1503
				if(links.length && dropped && dropped.iface.getWidget() && dropped.iface.getWidget().instanceOf(et2_calendar_event))
1504
				{
1505
					// Link the entries
1506
					egw.json(self.egw().getAppName()+".etemplate_widget_link.ajax_link.etemplate",
1507
						dropped.id.split('::').concat([links]),
1508
						function(result) {
1509
							if(result)
1510
							{
1511
								this.egw().message('Linked');
1512
							}
1513
						},
1514
						self,
1515
						true,
1516
						self
1517
					).sendRequest();
1518
				}
1519
			},true);
1520
1521
			drop_action.acceptedTypes = ['default','link'];
1522
			drop_action.hideOnDisabled = true;
1523
1524
			// Create the drop action for moving events between planner rows
1525
			var invite_action = function(action, source, target) {
1526
1527
				// Extract link IDs
1528
				var links = [];
1529
				var id = '';
1530
				for(var i = 0; i < source.length; i++)
1531
				{
1532
					// Check for no ID (invalid) or same manager (dragging an event)
1533
					if(!source[i].id) continue;
1534
					if(source[i].manager === target.manager)
1535
					{
1536
1537
						// Find the row, could have dropped on an event
1538
						var row = target.iface.getWidget();
1539
						while(target.parent && row.instanceOf && !row.instanceOf(et2_calendar_planner_row))
1540
						{
1541
							target = target.parent;
1542
							row = target.iface.getWidget();
1543
						}
1544
1545
						// Leave the helper there until the update is done
1546
						var loading = action.ui.helper.clone(true).appendTo(jQuery('body'));
1547
1548
						// and add a loading icon so user knows something is happening
1549
						if(jQuery('.calendar_timeDemo',loading).length == 0)
1550
						{
1551
							jQuery('.calendar_calEventHeader',loading).addClass('loading');
1552
						}
1553
						else
1554
						{
1555
							jQuery('.calendar_timeDemo',loading).after('<div class="loading"></div>');
1556
						}
1557
1558
						var event_data = egw.dataGetUIDdata(source[i].id).data;
1559
						et2_calendar_event.recur_prompt(event_data, function(button_id) {
1560
							if(button_id === 'cancel' || !button_id)
1561
							{
1562
								return;
1563
							}
1564
							var add_owner = [row.node.dataset.participants];
0 ignored issues
show
Bug introduced by
The variable row is changed as part of the for loop for example by target.iface.getWidget() on line 1538. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
1565
1566
							egw().json('calendar.calendar_uiforms.ajax_invite', [
1567
									button_id==='series' ? event_data.id : event_data.app_id,
0 ignored issues
show
Bug introduced by
The variable event_data is changed as part of the for loop for example by egw.dataGetUIDdata(source.i.id).data on line 1558. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
1568
									add_owner,
1569
									action.id === 'change_participant' ?
1570
										[source[i].iface.getWidget().getParent().node.dataset.participants] :
0 ignored issues
show
Bug introduced by
The variable i is changed as part of the for loop for example by i++ on line 1530. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
1571
										[]
1572
								],
1573
								function() { loading.remove();}
0 ignored issues
show
Bug introduced by
The variable loading is changed as part of the for loop for example by action.ui.helper.clone(t...ppendTo(jQuery("body")) on line 1546. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
1574
							).sendRequest(true);
1575
						});
1576
						// Ok, stop.
1577
						return false;
1578
					}
1579
				}
1580
			};
1581
1582
			drop_change_participant = mgr.addAction('drop', 'change_participant', egw.lang('Move to'), egw.image('participant'), invite_action,true);
1583
			drop_change_participant.acceptedTypes = ['calendar'];
1584
			drop_change_participant.hideOnDisabled = true;
1585
1586
			drop_invite = mgr.addAction('drop', 'invite', egw.lang('Invite'), egw.image('participant'), invite_action,true);
1587
			drop_invite.acceptedTypes = ['calendar'];
1588
			drop_invite.hideOnDisabled = true;
1589
		}
1590
		if(actionLinks.indexOf(drop_action.id) < 0)
1591
		{
1592
			actionLinks.push(drop_action.id);
1593
		}
1594
		actionLinks.push(drop_invite.id);
1595
		actionLinks.push(drop_change_participant.id);
1596
		
1597
		// Accept other links, and files dragged from the filemanager
1598
		// This does not handle files dragged from the desktop.  They are
1599
		// handled by et2_nextmatch, since it needs DOM stuff
1600
		if(drop_action.acceptedTypes.indexOf('link') == -1)
1601
		{
1602
			drop_action.acceptedTypes.push('link');
1603
		}
1604
1605
		// Don't re-add
1606
		if(drag_action == null)
1607
		{
1608
			// Create drag action that allows linking
1609
			drag_action = mgr.addAction('drag', 'egw_link_drag', egw.lang('link'), 'link', function(action, selected) {
1610
				// As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links
1611
				// TODO: Need to decide if we need to create a customized helper interface for links anyway
1612
				//return helper;
1613
				return null;
1614
			},true);
1615
		}
1616
		// The planner itself is not draggable, the action is there for the children
1617
		if(false && actionLinks.indexOf(drag_action.id) < 0)
1618
		{
1619
			actionLinks.push(drag_action.id);
1620
		}
1621
		drag_action.set_dragType(['link','calendar']);
1622
	},
1623
1624
	/**
1625
	 * Get all action-links / id's of 1.-level actions from a given action object
1626
	 *
1627
	 * Here we are only interested in drop events.
1628
	 *
1629
	 * @param actions
1630
	 * @returns {Array}
1631
	 */
1632
	_get_action_links: function(actions)
1633
	{
1634
		var action_links = [];
1635
1636
		// Only these actions are allowed without a selection (empty actions)
1637
		var empty_actions = ['add'];
1638
1639
		for(var i in actions)
1640
		{
1641
			var action = actions[i];
1642
			if(empty_actions.indexOf(action.id) !== -1 ||  action.type === 'drop')
1643
			{
1644
				action_links.push(typeof action.id !== 'undefined' ? action.id : i);
1645
			}
1646
		}
1647
		// Disable automatic paste action, it doesn't have what is needed to work
1648
		action_links.push({
1649
			"actionObj": 'egw_paste',
1650
			"enabled": false,
1651
			"visible": false
1652
		});
1653
		return action_links;
1654
	},
1655
1656
	/**
1657
	 * Show the current time while dragging
1658
	 * Used for resizing as well as drag & drop
1659
	 *
1660
	 * @param {type} element
1661
	 * @param {type} position
1662
	 * @param {type} height
1663
	 */
1664
	_drag_helper: function(element, position ,height)
1665
	{
1666
		var time = this._get_time_from_position(position.left, position.top);
1667
		element.dropEnd = time;
1668
		var formatted_time = jQuery.datepicker.formatTime(
1669
			egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm",
1670
			{
1671
				hour: time.getUTCHours(),
1672
				minute: time.getUTCMinutes(),
1673
				seconds: 0,
1674
				timezone: 0
1675
			},
1676
			{"ampm": (egw.preference("timeformat") === "12")}
1677
		);
1678
1679
		element.innerHTML = '<div class="calendar_d-n-d_timeCounter"><span class="calendar_timeDemo" >'+formatted_time+'</span></div>';
1680
1681
		//jQuery(element).width(jQuery(helper).width());
1682
	},
1683
1684
	/**
1685
	 * Handler for dropping an event on the timegrid
1686
	 *
1687
	 * @param {type} planner
1688
	 * @param {type} event
1689
	 * @param {type} ui
1690
	 */
1691
	_event_drop: function(planner, event,ui) {
1692
		var e = new jQuery.Event('change');
1693
		e.originalEvent = event;
1694
		e.data = {start: 0};
1695
		if (typeof this.dropEnd != 'undefined')
1696
		{
1697
			var drop_date = this.dropEnd.toJSON() ||false;
1698
1699
			var event_data = planner._get_event_info(ui.draggable);
1700
			var event_widget = planner.getWidgetById(event_data.widget_id);
1701
			if(event_widget)
1702
			{
1703
				event_widget._parent.date_helper.set_value(drop_date);
1704
				event_widget.options.value.start = new Date(event_widget._parent.date_helper.getValue());
1705
1706
				// Leave the helper there until the update is done
1707
				var loading = ui.helper.clone().appendTo(ui.helper.parent());
1708
				// and add a loading icon so user knows something is happening
1709
				jQuery('.calendar_timeDemo',loading).after('<div class="loading"></div>');
1710
1711
				event_widget.recur_prompt(function(button_id) {
1712
					if(button_id === 'cancel' || !button_id) return;
1713
					//Get infologID if in case if it's an integrated infolog event
1714
					if (event_data.app === 'infolog')
1715
					{
1716
						// If it is an integrated infolog event we need to edit infolog entry
1717
						egw().json('stylite_infolog_calendar_integration::ajax_moveInfologEvent',
1718
							[event_data.id, event_widget.options.value.start||false],
1719
							function() {loading.remove();}
1720
						).sendRequest(true);
1721
					}
1722
					else
1723
					{
1724
						//Edit calendar event
1725
						egw().json('calendar.calendar_uiforms.ajax_moveEvent', [
1726
								button_id==='series' ? event_data.id : event_data.app_id,event_data.owner,
1727
								event_widget.options.value.start,
1728
								planner.options.owner||egw.user('account_id')
1729
							],
1730
							function() { loading.remove();}
1731
						).sendRequest(true);
1732
					}
1733
				});
1734
			}
1735
		}
1736
	},
1737
1738
	/**
1739
	 * Use the egw.data system to get data from the calendar list for the
1740
	 * selected time span.
1741
	 *
1742
	 */
1743
	_fetch_data: function()
1744
	{
1745
		var value = [];
1746
		var fetch = false;
1747
		this.doInvalidate = false;
1748
1749
		for(var i = 0; i < this.registeredCallbacks.length; i++)
1750
		{
1751
			egw.dataUnregisterUID(this.registeredCallbacks[i],false,this);
1752
		}
1753
		this.registeredCallbacks.splice(0,this.registeredCallbacks.length);
1754
1755
		// Remember previous day to avoid multi-days duplicating
1756
		var last_data = [];
1757
1758
		var t = new Date(this.options.start_date);
1759
		var end = new Date(this.options.end_date);
1760
		do
1761
		{
1762
			value = value.concat(this._cache_register(t, this.options.owner, last_data));
1763
1764
			t.setUTCDate(t.getUTCDate() + 1);
1765
		}
1766
		while(t < end);
1767
1768
		this.doInvalidate = true;
1769
		return value;
1770
	},
1771
1772
	/**
1773
	 * Deal with registering for data cache
1774
	 *
1775
	 * @param Date t
0 ignored issues
show
Documentation introduced by
The parameter Date does not exist. Did you maybe forget to remove this comment?
Loading history...
1776
	 * @param String owner Calendar owner
0 ignored issues
show
Documentation introduced by
The parameter String does not exist. Did you maybe forget to remove this comment?
Loading history...
1777
	 */
1778
	_cache_register: function _cache_register(t, owner, last_data)
1779
	{
1780
		// Cache is by date (and owner, if seperate)
1781
		var date = t.getUTCFullYear() + sprintf('%02d',t.getUTCMonth()+1) + sprintf('%02d',t.getUTCDate());
1782
		var cache_id = app.classes.calendar._daywise_cache_id(date, owner);
1783
		var value = [];
1784
1785
		if(egw.dataHasUID(cache_id))
1786
		{
1787
			var c = egw.dataGetUIDdata(cache_id);
1788
			if(c.data && c.data !== null)
1789
			{
1790
				// There is data, pass it along now
1791
				for(var j = 0; j < c.data.length; j++)
1792
				{
1793
					if(last_data.indexOf(c.data[j]) === -1 && egw.dataHasUID('calendar::'+c.data[j]))
1794
					{
1795
						value.push(egw.dataGetUIDdata('calendar::'+c.data[j]).data);
1796
					}
1797
				}
1798
				last_data = c.data;
1799
			}
1800
		}
1801
		else
1802
		{
1803
			fetch = true;
1804
			// Assume it's empty, if there is data it will be filled later
1805
			egw.dataStoreUID(cache_id, []);
1806
		}
1807
		this.registeredCallbacks.push(cache_id);
1808
1809
		egw.dataRegisterUID(cache_id, function(data) {
1810
1811
			if(data && data.length)
1812
			{
1813
				var invalidate = true;
1814
1815
				// Try to determine rows interested
1816
				var labels = [];
1817
				var events = {};
1818
				if(this.grouper)
1819
				{
1820
					labels = this.grouper.row_labels.call(this);
1821
					invalidate = false;
1822
				}
1823
				
1824
				var im = this.getInstanceManager();
1825
				for(var i = 0; i < data.length; i++)
1826
				{
1827
					var event = egw.dataGetUIDdata('calendar::'+data[i]);
1828
1829
					if(!event) continue;
1830
					events = {};
1831
1832
					// Try to determine rows interested
1833
					if(event.data && this.grouper)
1834
					{
1835
						this.grouper.group.call(this, labels, events, event.data);
1836
					}
1837
					if(Object.keys(events).length > 0 )
1838
					{
1839
						for(var label_id in events)
1840
						{
1841
							var id = ""+labels[label_id].id;
1842
							if(typeof this.cache[id] === 'undefined')
1843
							{
1844
								this.cache[id] = [];
1845
							}
1846
							if(this.cache[id].indexOf(event.data.row_id) === -1)
1847
							{
1848
								this.cache[id].push(event.data.row_id);
1849
							}
1850
							if (this._deferred_row_updates[id])
1851
							{
1852
								window.clearTimeout(this._deferred_row_updates[id]);
1853
							}
1854
							this._deferred_row_updates[id] = window.setTimeout(jQuery.proxy(this._deferred_row_update,this,id),this.DEFERRED_ROW_TIME);
1855
						}
1856
					}
1857
					else
1858
					{
1859
						// Could be an event no row is interested in, could be a problem.
1860
						// Just redraw everything
1861
						invalidate = true;
1862
						continue;
1863
					}
1864
1865
					// If displaying by category, we need the infolog (or other app) categories too
1866
					if(event && event.data && event.data.app && this.options.group_by == 'category')
1867
					{
1868
						// Fake it to use the cache / call
1869
						et2_selectbox.cat_options({
1870
							_type:'select-cat',
1871
							getInstanceManager: function() {return im;}
1872
						}, {application:event.data.app||'calendar'});
1873
1874
						// Get CSS too
1875
						egw.includeCSS('/api/categories.php?app='+event.data.app);
1876
					}
1877
				}
1878
1879
				if(invalidate)
1880
				{
1881
					this.invalidate(false);
1882
				}
1883
			}
1884
		}, this, this.getInstanceManager().execId,this.id);
1885
		
1886
		return value;
1887
	},
1888
1889
	/**
1890
	 * Because users may be participants in various events and the time it takes
1891
	 * to create many events, we don't want to update a row too soon - we may have
1892
	 * to re-draw it if we find the user / category in another event.  Pagination
1893
	 * makes this worse.  We wait a bit before updating the row to avoid
1894
	 * having to re-draw it multiple times.
1895
	 *
1896
	 * @param {type} id
1897
	 * @returns {undefined}
1898
	 */
1899
	_deferred_row_update: function(id) {
1900
		// Something's in progress, skip
1901
		if(!this.doInvalidate) return;
1902
1903
		this.grid.height(0);
1904
		
1905
		var id_list = typeof id === 'undefined' ? Object.keys(this.cache) : [id];
1906
		for(var i = 0; i < id_list.length; i++)
1907
		{
1908
			var cache_id = id_list[i];
1909
			var row = this.getWidgetById('planner_row_'+cache_id);
1910
1911
			window.clearTimeout(this._deferred_row_updates[cache_id]);
1912
			delete this._deferred_row_updates[cache_id];
1913
1914
			if(row)
1915
			{
1916
				row._data_callback(this.cache[cache_id]);
1917
				row.set_disabled(this.options.hide_empty && this.cache[cache_id].length === 0);
1918
			}
1919
			else
1920
			{
1921
				break;
1922
			}
1923
		}
1924
1925
		// Updating the row may push things longer, update length
1926
		// Add 1 to keep the scrollbar, otherwise we need to recalculate the
1927
		// header widths too.
1928
		this.grid.height(this.rows[0].scrollHeight+1);
1929
		
1930
		// Adjust header if there's a scrollbar - Firefox needs this re-calculated,
1931
		// otherwise the header will be missing the margin space for the scrollbar
1932
		// in some cases
1933
		if(this.rows.children().last().length)
1934
		{
1935
			this.gridHeader.css('margin-right', (this.rows.width() - this.rows.children().last().width()) + 'px');
1936
		}
1937
	},
1938
1939
	/**
1940
	 * Provide specific data to be displayed.
1941
	 * This is a way to set start and end dates, owner and event data in once call.
1942
	 *
1943
	 * @param {Object[]} events Array of events, indexed by date in Ymd format:
1944
	 *	{
1945
	 *		20150501: [...],
1946
	 *		20150502: [...]
1947
	 *	}
1948
	 *	Days should be in order.
1949
	 *
1950
	 */
1951
	set_value: function set_value(events)
1952
	{
1953
		if(typeof events !== 'object') return false;
1954
1955
		this._super.apply(this, arguments);
1956
1957
		// Planner uses an array, not map
1958
		var val = this.value;
1959
		var array = [];
1960
		Object.keys(this.value).forEach(function (key) {
1961
			array.push(val[key]);
1962
		});
1963
		this.value = array;
1964
	},
1965
1966
	/**
1967
	 * Change the start date
1968
	 * Planner view uses a date object internally
1969
	 *
1970
	 * @param {string|number|Date} new_date New starting date
1971
	 * @returns {undefined}
1972
	 */
1973
	set_start_date: function set_start_date(new_date)
1974
	{
1975
		this._super.apply(this, arguments);
1976
		this.options.start_date = new Date(this.options.start_date);
1977
	},
1978
1979
	/**
1980
	 * Change the end date
1981
	 * Planner view uses a date object internally
1982
	 *
1983
	 * @param {string|number|Date} new_date New end date
1984
	 * @returns {undefined}
1985
	 */
1986
	set_end_date: function set_end_date(new_date)
1987
	{
1988
		this._super.apply(this, arguments);
1989
		this.options.end_date = new Date(this.options.end_date);
1990
	},
1991
1992
	/**
1993
	 * Change how the planner is grouped
1994
	 *
1995
	 * @param {string|number} group_by 'user', 'month', or an integer category ID
1996
	 * @returns {undefined}
1997
	 */
1998
	set_group_by: function set_group_by(group_by)
1999
	{
2000
		if(isNaN(group_by) && typeof this.groupers[group_by] === 'undefined')
2001
		{
2002
			throw new Error('Invalid group_by "'+group_by+'"');
2003
		}
2004
		var old = this.options.group_by;
2005
		this.options.group_by = ''+group_by;
2006
2007
		this.grouper = this.groupers[isNaN(this.options.group_by) ? this.options.group_by : 'category'];
2008
		
2009
		if(old !== this.options.group_by && this.isAttached())
2010
		{
2011
			this.invalidate(true);
2012
		}
2013
	},
2014
2015
	/**
2016
	 * Turn on or off the visibility of weekends
2017
	 *
2018
	 * @param {boolean} weekends
2019
	 */
2020
	set_show_weekend: function set_show_weekend(weekends)
2021
	{
2022
		weekends = weekends ? true : false;
2023
		if(this.options.show_weekend !== weekends)
2024
		{
2025
			this.options.show_weekend = weekends;
2026
			if(this.isAttached())
2027
			{
2028
				this.invalidate();
2029
			}
2030
		}
2031
	},
2032
2033
	/**
2034
	 * Turn on or off the visibility of hidden (empty) rows
2035
	 *
2036
	 * @param {boolean} hidden
2037
	 */
2038
	set_hide_empty: function set_hide_empty(hidden)
2039
	{
2040
		this.options.hide_empty = hidden;
2041
	},
2042
2043
	/**
2044
	 * Call change handler, if set
2045
	 *
2046
	 * @param {type} event
2047
	 */
2048
	change: function(event) {
2049
		if (this.onchange)
2050
		{
2051
			if(typeof this.onchange == 'function')
2052
			{
2053
				// Make sure function gets a reference to the widget
2054
				var args = Array.prototype.slice.call(arguments);
2055
				if(args.indexOf(this) == -1) args.push(this);
2056
2057
				return this.onchange.apply(this, args);
2058
			} else {
2059
				return (et2_compileLegacyJS(this.options.onchange, this, _node))();
2060
			}
2061
		}
2062
	},
2063
2064
	/**
2065
	 * Call event change handler, if set
2066
	 *
2067
	 * @param {type} event
2068
	 * @param {type} dom_node
2069
	 */
2070
	event_change: function(event, dom_node) {
2071
		if (this.onevent_change)
2072
		{
2073
			var event_data = this._get_event_info(dom_node);
2074
			var event_widget = this.getWidgetById(event_data.widget_id);
2075
			et2_calendar_event.recur_prompt(event_data, jQuery.proxy(function(button_id, event_data) {
2076
				// No need to continue
2077
				if(button_id === 'cancel') return false;
2078
2079
				if(typeof this.onevent_change == 'function')
2080
				{
2081
					// Make sure function gets a reference to the widget
2082
					var args = Array.prototype.slice.call(arguments);
2083
2084
					if(args.indexOf(event_widget) == -1) args.push(event_widget);
2085
2086
					// Put button ID in event
2087
					event.button_id = button_id;
2088
2089
					return this.onevent_change.apply(this, [event, event_widget, button_id]);
2090
				} else {
2091
					return (et2_compileLegacyJS(this.options.onevent_change, event_widget, dom_node))();
2092
				}
2093
			},this));
2094
		}
2095
		return false;
2096
	},
2097
2098
	/**
2099
	 * Click handler calling custom handler set via onclick attribute to this.onclick
2100
	 *
2101
	 * This also handles all its own actions, including navigation.  If there is
2102
	 * an event associated with the click, it will be found and passed to the
2103
	 * onclick function.
2104
	 *
2105
	 * @param {Event} _ev
2106
	 * @returns {boolean} Continue processing event (true) or stop (false)
2107
	 */
2108
	click: function(_ev)
2109
	{
2110
		var result = true;
2111
2112
		// Drag to create in progress
2113
		if(this.drag_create.start !== null) return;
2114
		
2115
		// Is this click in the event stuff, or in the header?
2116
		if(!this.options.readonly && this.gridHeader.has(_ev.target).length === 0 && !jQuery(_ev.target).hasClass('calendar_plannerRowHeader'))
2117
		{
2118
			// Event came from inside, maybe a calendar event
2119
			var event = this._get_event_info(_ev.originalEvent.target);
2120
			if(typeof this.onclick == 'function')
2121
			{
2122
				// Make sure function gets a reference to the widget, splice it in as 2. argument if not
2123
				var args = Array.prototype.slice.call(arguments);
2124
				if(args.indexOf(this) == -1) args.splice(1, 0, this);
2125
2126
				result = this.onclick.apply(this, args);
2127
			}
2128
2129
			if(event.id && result && !this.options.disabled && !this.options.readonly)
2130
			{
2131
				et2_calendar_event.recur_prompt(event);
2132
2133
				return false;
2134
			}
2135
			else if (!event.id)
2136
			{
2137
				// Clicked in row, but not on an event
2138
				// Default handler to open a new event at the selected time
2139
				if(jQuery(event.target).closest('.calendar_eventRows').length == 0)
2140
				{
2141
					// "Invalid" times, from space after the last planner row, or header
2142
					var date = this._get_time_from_position(_ev.pageX - this.grid.offset().left, _ev.pageY - this.grid.offset().top);
2143
				}
2144
				else if(this.options.group_by == 'month')
2145
				{
2146
					var date = this._get_time_from_position(_ev.clientX, _ev.clientY);
2147
				}
2148
				else
2149
				{
2150
					var date = this._get_time_from_position(_ev.offsetX, _ev.offsetY);
2151
				}
2152
				var row = jQuery(_ev.target).closest('.calendar_plannerRowWidget');
2153
				var data = row.length ? row[0].dataset : {};
2154
				this.egw().open(null, 'calendar', 'add', jQuery.extend({
2155
					start: date.toJSON(),
2156
					hour: date.getUTCHours(),
2157
					minute: date.getUTCMinutes()
2158
				},data) , '_blank');
2159
				return false;
2160
			}
2161
			return result;
2162
		}
2163
		else if (this.gridHeader.has(_ev.target).length > 0 && !jQuery.isEmptyObject(_ev.target.dataset) ||
2164
			jQuery(_ev.target).hasClass('calendar_plannerRowHeader') && !jQuery.isEmptyObject(_ev.target.dataset))
2165
		{
2166
			// Click on a header, we can go there
2167
			_ev.data = jQuery.extend({},_ev.target.parentNode.dataset, _ev.target.dataset);
2168
			for(var key in _ev.data)
2169
			{
2170
				if(!_ev.data[key])
2171
				{
2172
					delete _ev.data[key];
2173
				}
2174
			}
2175
			app.calendar.update_state(_ev.data);
2176
		}
2177
		else if (!this.options.readonly)
2178
		{
2179
			// Default handler to open a new event at the selected time
2180
			// TODO: Determine date / time more accurately from position
2181
			this.egw().open(null, 'calendar', 'add', {
2182
				date: _ev.target.dataset.date || this.options.start_date.toJSON(),
2183
				hour: _ev.target.dataset.hour || this.options.day_start,
2184
				minute: _ev.target.dataset.minute || 0
2185
			} , '_blank');
2186
			return false;
2187
		}
2188
	},
2189
2190
	/**
2191
	 * Get time from position
2192
	 *
2193
	 * @param {number} x
2194
	 * @param {number} y
2195
	 * @returns {Date|Boolean} A time for the given position, or false if one
2196
	 *	could not be determined.
2197
	 */
2198
	_get_time_from_position: function(x,y) {
2199
2200
		x = Math.round(x);
2201
		y = Math.round(y);
2202
		
2203
		// Round to user's preferred event interval
2204
		var interval = egw.preference('interval','calendar') || 30;
2205
2206
		// Relative horizontal position, as a percentage
2207
		var rel_x = Math.min(x / jQuery('.calendar_eventRows',this.div).width(),1);
2208
2209
		// Relative time, in minutes from start
2210
		var rel_time = 0;
2211
2212
		var day_header = jQuery('.calendar_plannerScaleDay',this.headers);
2213
2214
		// Simple math, the x is offset from start date
2215
		if(this.options.group_by !== 'month' && (
2216
			// Either all days are visible, or only 1 day (no day header)
2217
			this.options.show_weekend || day_header.length === 0
2218
		))
2219
		{
2220
			rel_time = (new Date(this.options.end_date) - new Date(this.options.start_date))*rel_x/1000;
2221
			this.date_helper.set_value(this.options.start_date.toJSON());
2222
		}
2223
		// Not so simple math, need to account for missing days
2224
		else if(this.options.group_by !== 'month' && !this.options.show_weekend)
2225
		{
2226
			// Find which day
2227
			if(day_header.length === 0) return false;
2228
			var day = document.elementFromPoint(
2229
				day_header.offset().left + rel_x * this.headers.innerWidth(),
2230
				day_header.offset().top
2231
			);
2232
2233
			// Use day, and find time in that day
2234
			if(day && day.dataset && day.dataset.date)
2235
			{
2236
				this.date_helper.set_value(day.dataset.date);
2237
				rel_time = ((x - jQuery(day).position().left) / jQuery(day).outerWidth(true)) * 24*60;
2238
				this.date_helper.set_minutes(Math.round(rel_time/interval) * interval);
2239
				return new Date(this.date_helper.getValue());
2240
			}
2241
			return false;
2242
		}
2243
		else
2244
		{
2245
			// Find the correct row so we know which month, then get the offset
2246
			var hidden_nodes = [];
2247
			var row = null;
2248
			// Hide any drag or tooltips that may interfere
2249
			do
2250
			{
2251
				row = document.elementFromPoint(x, y);
2252
				if(this.div.has(row).length == 0)
2253
				{
2254
					hidden_nodes.push(jQuery(row).hide());
2255
				}
2256
				else
2257
				{
2258
					break;
2259
				}
2260
			} while(row && row.nodeName !== 'BODY');
2261
			if(!row) return false;
2262
			
2263
			// Restore hidden nodes
2264
			for(var i = 0; i < hidden_nodes.length; i++)
2265
			{
2266
				hidden_nodes[i].show();
2267
			}
2268
			row = jQuery(row).closest('.calendar_plannerRowWidget');
2269
2270
2271
			var row_widget = null;
2272
			for(var i = 0; i < this._children.length && row.length > 0; i++)
2273
			{
2274
				if(this._children[i].div[0] == row[0])
2275
				{
2276
					row_widget = this._children[i];
2277
					break;
2278
				}
2279
			}
2280
			if(row_widget)
2281
			{
2282
				// Not sure where the extra -1 and +2 are coming from, but it makes it work out
2283
				// in FF & Chrome
2284
				rel_x = Math.min((x-row_widget.rows.offset().left-1)/(row_widget.rows.width()+2),1);
2285
2286
				// 2678400 is the number of seconds in 31 days
2287
				rel_time = (2678400)*rel_x;
2288
				this.date_helper.set_value(row_widget.options.start_date.toJSON());
2289
			}
2290
			else
2291
			{
2292
				return false;
2293
			}
2294
		}
2295
		if(rel_time < 0) return false;
2296
2297
		this.date_helper.set_minutes(Math.round(rel_time / (60 * interval))*interval);
2298
2299
		return new Date(this.date_helper.getValue());
2300
	},
2301
2302
	/**
2303
	 * Mousedown handler to support drag to create
2304
	 *
2305
	 * @param {jQuery.Event} event
2306
	 */
2307
	_mouse_down: function(event)
2308
	{
2309
		// Only left mouse button
2310
		if(event.which !== 1) return;
2311
		
2312
		// Ignore headers
2313
		if(this.headers.has(event.target).length !== 0) return false;
2314
		
2315
		// Get time at mouse
2316
		if(this.options.group_by === 'month')
2317
		{
2318
			var time = this._get_time_from_position(event.clientX, event.clientY);
2319
		}
2320
		else
2321
		{
2322
			var time = this._get_time_from_position(event.offsetX, event.offsetY);
2323
		}
2324
		if(!time) return false;
2325
2326
		// Find the correct row so we know the parent
2327
		var row = event.target.closest('.calendar_plannerRowWidget');
2328
		for(var i = 0; i < this._children.length && row; i++)
2329
		{
2330
			if(this._children[i].div[0] === row)
2331
			{
2332
				this.drag_create.parent = this._children[i];
2333
				// Clear cached events for re-layout
2334
				this._children[i]._cached_rows = [];
2335
				break;
2336
			}
2337
		}
2338
		if(!this.drag_create.parent) return false;
2339
2340
		this.div.css('cursor', 'ew-resize');
2341
2342
		return this._drag_create_start(jQuery.extend({},this.drag_create.parent.node.dataset,{date: time.toJSON()}));
2343
	},
2344
2345
	/**
2346
	 * Mouseup handler to support drag to create
2347
	 *
2348
	 * @param {jQuery.Event} event
2349
	 */
2350
	_mouse_up: function(event)
2351
	{
2352
		// Get time at mouse
2353
		if(this.options.group_by === 'month')
2354
		{
2355
			var time = this._get_time_from_position(event.clientX, event.clientY);
2356
		}
2357
		else
2358
		{
2359
			var time = this._get_time_from_position(event.offsetX, event.offsetY);
2360
		}
2361
2362
		return this._drag_create_end(time ? {date: time.toJSON()} : false);
2363
	},
2364
2365
	/**
2366
	 * Code for implementing et2_IDetachedDOM
2367
	 *
2368
	 * @param {array} _attrs array to add further attributes to
2369
	 */
2370
	getDetachedAttributes: function(_attrs) {
2371
		_attrs.push('start_date','end_date');
2372
	},
2373
2374
	getDetachedNodes: function() {
2375
		return [this.getDOMNode()];
2376
	},
2377
2378
	setDetachedAttributes: function(_nodes, _values) {
2379
		this.div = jQuery(_nodes[0]);
2380
2381
		if(_values.start_date)
2382
		{
2383
			this.set_start_date(_values.start_date);
2384
		}
2385
		if(_values.end_date)
2386
		{
2387
			this.set_end_date(_values.end_date);
2388
		}
2389
	},
2390
2391
	// Resizable interface
2392
	resize: function ()
2393
	{
2394
		// Take the whole tab height
2395
		var height = Math.min(jQuery(this.getInstanceManager().DOMContainer).height(),jQuery(this.getInstanceManager().DOMContainer).parent().innerHeight());
2396
2397
		// Allow for toolbar
2398
		height -= jQuery('#calendar-toolbar',this.div.parents('.egw_fw_ui_tab_content')).outerHeight(true);
2399
2400
		this.options.height = height;
2401
		this.div.css('height', this.options.height);
2402
		// Set height for rows
2403
		this.rows.height(this.div.height() - this.headers.outerHeight());
2404
		
2405
		this.grid.height(this.rows[0].scrollHeight);
2406
	},
2407
2408
	/**
2409
	 * Set up for printing
2410
	 *
2411
	 * @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up
2412
	 *  (waiting for data)
2413
	 */
2414
	beforePrint: function() {
2415
2416
		if(this.disabled || !this.div.is(':visible'))
2417
		{
2418
			return;
2419
		}
2420
		this.rows.css('overflow-y', 'visible');
2421
2422
		var rows = jQuery('.calendar_eventRows');
2423
		var width = rows.width();
2424
		var events = jQuery('.calendar_calEvent', rows)
2425
				.each(function() {
2426
					var event = jQuery(this);
2427
					event.width((event.width() / width) * 100 + '%')
2428
				});
2429
2430
	},
2431
2432
	/**
2433
	 * Reset after printing
2434
	 */
2435
	afterPrint: function() {
2436
		this.rows.css('overflow-y', 'auto');
2437
	}
2438
});}).call(this);
2439
et2_register_widget(et2_calendar_planner, ["calendar-planner"]);
2440